iT邦幫忙

2022 iThome 鐵人賽

DAY 29
3
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 29

Day 29: Vue 響應式基礎 - watch & computed 不踩坑

  • 分享至 

  • xImage
  •  

前言

Day 9 和 10 的文章中,我們提到 Vue 3 響應式基礎 - reactive & ref,主要用來攔截資料的讀取跟寫入,讓 Vue 去更新或執行依賴。

而 computed 和 watch 就是讓我們能在響應式數據被改變的時候,去更新加工的值或是執行其他程式碼。

Computed

computed 通常拿來做響應式資料的加工計算,具備緩存特性,能提昇效能,只在響應式資料改變時,重新執行運算。

Syntax

  • 傳入 getter function:
    結果為唯讀,不可以對 computed 重新賦值
    const computedValue = computed(getterFunction);
    
  • 傳入一組 getter 和 setter
    可以讀取和寫入,每次會調用 getter 和 setter
    const computedValue = computed({
      // getter
      get() {
        //通常會回傳 reactive 資料加工後
        return reactiveValue 的加工
      },
      // setter
      set(newValue) {
        //通常會將新寫入的值賦值給相關的 reactive 資料
        reactiveValue = newValue
      },
    });
    
    使用範例
    import { ref, computed } from 'vue'
    
    const firstName = ref('John')
    const lastName = ref('Doe')
    
    const fullName = computed({
      // getter
      get() {
        return firstName.value + ' ' + lastName.value
      },
      // setter
      set(newValue) {
        // 使用解構賦值
        [firstName.value, lastName.value] = newValue.split(' ')
      }
    })
    
    fullName.value = 'Keanu Reeves'
    

Computed 在依賴的響應式資料變動時,會重新計算結果並回傳

關於響應式資料的改變
基本型別比較的是值,物件型別比較的是參照。
基本型別被修改就會觸發,物件型別只在參照改變時觸發,單純修改原物件(新增屬性到物件、push 新項目到陣列),都不會觸發重新計算。

何為依賴的響應式資料?

  1. 由 ref 或 reactive 定義的響應式資料
    需要透過 ref 物件或 reactive 物件進行讀取,才可以確實響應。
    例如:將 ref 的 value 取出來存成區域變數,會失去響應性,value 變成一般純值。

    下面的範例沒有響應性,改動 count.value 不會觸發 computed。

    const counts = ref(0);
    const value = counts.value;
    
    // 沒有響應性,改動 count.value 不會觸發 computed
    const computedCounts = computed(() => {
      console.log("我在計算新的 computedCounts", value);
      return `${value} 次`;
    });
    
  2. 只要在 getter 執行時讀取到的響應式資料,都是 computed 的依賴,不是跟計算結果有依賴關係,才算是依賴。

    下面的範例每次改變 counts.value 都會印出"嗨":

    const computedCounts = computed(() => {
      console.log("我在計算新的 computedCounts", counts.value);
      return "嗨";
    });
    

用 computed 來 computed

computed 是基於 Ref 物件進行擴展的,類別名稱是 ComputedRefImpl,如果想要用一個 computed 結果,來計算另一個 computed,需要用 .value 取得 computed 的值。

const price = ref(1000);
const afterDiscount = computed(() => price.value * 0.9);
const reward = computed(() => afterDiscount.value * 0.1);

Computed v.s Methods

  1. 是否暫存計算結果:
    • computed 屬性會將計算後的結果暫存起來,如果他使用到的屬性沒有更新,computed 就不會被重複執行。
    • method 每次使用到都會呼叫。
  2. 在 template 中的用法:
    • computed 不用呼叫,直接帶方法名稱
    • method 要加()進行呼叫
  3. 可否帶參數?(承上)
    • computed 不能帶參數
    • method 可以帶參數

watcher

Watcher 可以用來監視(響應式)狀態/資料的改變,並觸發 callback 函式。

Syntax

watch(reactiveData, callback(newReactiveData, oldReactiveData))
  • callback 可以是同步或非同步
    • newReactiveData: 觸發 watcher 前的 reactiveData
    • oldReactiveData: 觸發 watcher 後的 reactiveData
  • 建議在 setup() 執行期間,同步進行註冊,如在異步期間進行註冊,無法綁定元件實例,需要自己手動清除。(好奇原因可以去看 Day22 的文章)

可以 watch 的資料類型

watch() 函式的第一個參數要放想要監聽的資料來源,當然監聽的資料需要具有響應式,vue 才能觀察他的變化。
可以接受的資料包括:

  • ref、computed ref
  • reactvie object
  • getter function
  • array(項目需為以上來源)

接下來稍微說明不同資料類型的注意事項:

  • ref、computed ref
    • 要直接監聽 RefImplComputedRefImpl
    • 不能監聽 RefImpl.valueComputedRefImpl.value,因為拿到的是純值
  • reactvie object
    • 預設深層監聽,巢狀物件內部的屬性改變也會觸發 callback
    • reactive 物件下的屬性為基本型別,不能單獨監聽
    • reactive 物件下的屬性是物件,這個巢狀物件通常會是 Proxy 物件,所以可以監聽
  • getter function
    watch(
      () => x.value + y.value,
      (sum) => {
        console.log(`sum of x + y is: ${sum}`)
      }
    )
    
  • array(項目需為以上來源)
    watch([x, () => y.value], ([newX, newY]) => {
      console.log(`x is ${newX} and y is ${newY}`)
    })
    

不管你的響應式資料是 ref、computed ref 或 reactvie object,重點是傳進入的 source:

  • 儲存狀態(.value 或內部屬性 obj.property)為基本型別,都不能單獨監聽
  • 儲存狀態(.value 或內部屬性 obj.property)為物件型別,這個「內部物件」通常會是 Proxy 物件,是的話,就可以監聽。

監聽 reactive 物件

想要監聽響應式物件資料的情況下,可以選擇:

  1. 直接監聽 reactive object
  2. 使用 getter 回傳物件

但這兩個方式是有差異的,監聽 reactive object 會監聽內部屬性的變化,但監聽回傳物件的 getter 時,預設監測的是參照(reference)是否改變

範例
在 input 利用 v-model 綁定 reactive 物件內的物件,並嘗試分別寫了兩種 watcher。

<div>
  <h4>reactive 物件中的物件</h4>
  <input type="number" v-model="nestedReactiveObj.obj.input" />
  <p>{{ nestedReactiveObj.obj.input }}</p>
</div>
const nestedReactiveObj = reactive({
  name: "nestedReactiveObj",
  obj: { name: "nestedAndNestedReactiveObj", input: "" },
});

watch(nestedReactiveObj, () => {
  console.log("來自監測整個物件的 watcher");
});

watch(
  () => nestedReactiveObj.obj,
  () => {
    console.log("來自監測 getter 的 watcher");
  }
);

結果
在這種使用情境下(監聽 input 的數值去處發 callback),來自監測 getter 的 watcher 是不會被觸發的,他只會監測參照(reference)是否改變。

解決方法
將 watcher 的 {deep: true} 設定為 true,就可以監聽內部數據的改變,但要小心使用,深層監聽需要遍曆物件所有屬性,在資料很龐大的狀況下會影響效能。

watchEffect

  • watch 只有在指定監聽的資料改變時的時候會觸發 callback。
  • watchEffect 在建立追蹤的時候,會立即執行一次 callback,並在 callback 執行過程中,所有讀取到的響應式資料有變化時,都會再執行一次 callback

很適合在 watchEffect 內發 API,取得資料做畫面渲染。

以 f2e 旅館預約 API 為例:

const rooms = ref([]);

watchEffect(async () => {
  const response = await hotelAPI.GET("/rooms");
  rooms.value = response.items;
  console.log(rooms.value);
});

注意
watchEffect() 只追蹤同步執行期間相依賴的資料,一旦遇到 await,接下來的程式碼就被丟到 event loop,非同步時期才使用到的資料內容,都不會被追蹤。

computed v.s watch 系列總結

computed 和 watcher 都可以根據響應式狀態的變化,去執行相對應的動作:

computed watch watchEffect
偵測一個或多個值 只能偵測一個值 偵測一個或多個值,非同步期間的響應式不會被追蹤
一開始會執行一次(有使用到的話) 值變動時執行 一開始會執行一次
不能帶參數 參數一為新值、參數二為舊值 不能帶參數
只能處理同步 callback 可以是同步或非同步 callback 可以是同步或非同步
side effect 只能在 setter 觸發 可觸發 side effect 可觸發 side effect
回傳結果 執行 callback function 執行 callback function
  • Computed 適合用來單純加工資料,尤其是模版上會使用到的狀態,可以直接拿 computed 回傳的值進行渲染。
  • watch 追蹤明確的資料,而 callback 可以拿到追蹤資料進行處理,例如:watch 路由參數的變化重新發 API。
  • watchEffect 會追蹤所有相依的響應式資料,並且會立即執行,在需要以上特性時,可以讓程式碼更簡潔。

上一篇
Day 28: Quasar 新手指南 - 從安裝到使用
下一篇
Day 30: Vue 3 響應式原理 - effect 如何響應 (無敵簡化版)
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言